"""
Copyright (c) 2009 James Lindstorff
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
"""

import hashlib
import socket

CLIS_SYNTAX = 100
CLIS_UNKNOWN = 101
CLIS_UNIMPL = 102
CLIS_TOOFEW = 104
CLIS_TOOMANY = 105
CLIS_PARAM = 106
CLIS_AUTH = 107
CLIS_OK = 200
CLIS_CANT = 300
CLIS_COMMS = 400
CLIS_CLOSE = 500

class VarnishProtocol(object):
    """Thin wrapper on Varnish management port able to connect
    with authentication but otherwise only able to send text to the
    management port and return the status code and text. For a higher level
    interface use VarnishCLI.
    """
    def __init__(self, host='localhost', port=6082, secret=None):
        """Initialize with host, port and secret of the Varnish management port
        to communicate with. Defaults are localhost port 6082 secret None.
        If Varnish is started with -S secret will need to contain a reference
        to a file corresponding to the one Varnish was started with."""
        self.__address = (host, port)
        self.__socket = None
        self.__secret = secret


    def __readSecret(self):
        """Read the secret referenced by secret in the constructor.
        Returns the content of the file in a string."""
        file = None
        try:
            file = open(self.__secret, 'r')
            secret = file.read()
            return secret
        finally:
            if not file is None:
                file.close()


    def __challenge(self, challenge):
        """Extract the challenge from the raw Varnish challenge response.
        The challenge is the text up until the first newline."""
        firstNewline = challenge.find("\n")
        return challenge[:firstNewline]


    def __digestFromChallenge(self, challenge):
        """Create the digest response (SHA256 hex value) for a Varnish
        authentication challenge and secret. The secret is read from the
        file referenced by secret in the constructor. Returns a lower-case
        hex representation of the SHA256 digest."""

        if self.__secret is None:
            #TODO: We can't authenticate because we don't have a secret.
            pass
        
        secret = self.__readSecret()

        digester = hashlib.sha256()
        digester.update(challenge)
        digester.update("\n")
        digester.update(secret)
        digester.update(challenge)
        digester.update("\n")
        digest = digester.hexdigest()
        return digest.lower()


    def __open(self):
        """Opens a socket connection to the management port. This includes
        removing the banner send of cli_banner parameter is on and handling
        authentication if Varnish is started with -S."""
        if self.__socket is None:
            self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.__socket.connect(self.__address)
            # If Varnish is running without authentication or banner turned on it will
            # not send a response on connect therefore we start by reading the
            # response header with a timeout.
            self.__socket.settimeout(0.5)
            try:
                (statuscode, bodylength) = self.__header()
            except socket.timeout:
                (statuscode, bodylength) = (CLIS_OK, 0)

            # Place socket back into blocking mode
            self.__socket.settimeout(None)

            # Auth is signaled by CLIS_AUTH
            if CLIS_AUTH == statuscode:
                challengeResponse = self.__body(bodylength)
                challenge = self.__challenge(challengeResponse)
                digest = self.__digestFromChallenge(challenge)

                (statuscode, unused) = self.send("auth "+ digest)
                # If cli_banner is off a correct auth is answered with a 200 0
                # otherwise it just answers the banner which is read and
                # the connection is ready to use in request response mode.


                # TODO: If we don't get a 200 here we failed to auth

            elif CLIS_OK == statuscode and 0 < bodylength:
                # Banner and just dump the result
                self.__body(bodylength)



    def __close(self):
        """Close the connection to the management port."""
        if not self.__socket is None:
            self.__socket.close()
            self.__socket = None


    def __header(self):
        """Read the 13 byte CLI header. Returning (statuscode, bodylength)"""
        header = self.__socket.recv(13)
        statuscode = int(header[0:3])
        bodylength = int(header[4:12])
        return (statuscode, bodylength)


    def __body(self, bodylength):
        """Recieve the response body by reading bodylength bytes from the socket.
        Returning the responsebody as a string."""
        bufferSize = 1024
        body = ''
        remaining = bodylength
        while(0 < remaining):
            bufsize = min(remaining, bufferSize)
            buffer = self.__socket.recv(bufsize)
            body = body + buffer
            remaining = remaining - bufsize
        self.__socket.recv(1) # Remove the trailing newline
        return body


    def __recieve(self):
        """Recieves response from the management port.
        Returns a tuple (statuscode, responsebody)."""
        (statuscode, bodylength) = self.__header()
        responsebody = self.__body(bodylength)
        return (statuscode, responsebody)


    def send(self, command):
        """Send command to the management port
        returning (statuscode, responsetext)."""
        self.__open()
        self.__socket.sendall(command + '\n')
        (statuscode, text) = self.__recieve()
        # If Varnish sends a CLIS_CLOSE it has closed the connection
        if CLIS_CLOSE == statuscode:
            self.__close()
        return (statuscode, text)
